Skip to content

fix(security): require signed tenant claims on admin endpoints - #268

Draft
seonghobae wants to merge 54 commits into
fix/pii-logging-16240128950440010639from
fix/admin-endpoint-auth-clean
Draft

fix(security): require signed tenant claims on admin endpoints#268
seonghobae wants to merge 54 commits into
fix/pii-logging-16240128950440010639from
fix/admin-endpoint-auth-clean

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Security objective

Make every administrative conversion-job endpoint fail closed under signed tenant claims, least-privilege permissions, tenant-scoped persistence queries, atomic tenant-scoped mutations, immutable job identity, tenant-bound secondary indexes, and privacy-safe audit evidence.

This draft supersedes closed #266 and is stacked directly on authoritative parent #270 through base branch fix/pii-logging-16240128950440010639. Administrative audit evidence reuses and preserves the parent’s dedicated keyed pseudonymization contract.

Security boundary

  • require admin:read for tenant-scoped listing and admin:write for delete and retry;
  • return 503 before repository access when the signed-claim HMAC verifier is absent or shorter than 32 UTF-8 bytes;
  • verify missing, malformed, expired, invalid-signature, and insufficient-permission claims through the real authorization service;
  • select list data through findAllByTenantId before job objects cross the repository/service boundary;
  • pass verified TenantContext into delete and retry service mutations;
  • use atomic deleteByTenantAndId and retryDeadLetteredForTenant contracts whose compatibility defaults fail closed without global lookup or legacy mutation;
  • conceal missing, null-ID, and cross-tenant targets identically without artifact deletion, state transition, deduplication, or worker enqueue;
  • attempt artifact removal only after an owned repository deletion succeeds and enqueue work only after an owned retry transition succeeds;
  • bind content-hash indexes to tenant plus hash and validate indexed ownership;
  • reserve each accepted conversion-job UUID as a permanent lifecycle identity: saving or deduplicating the exact same live object may be idempotent, but distinct live objects and deleted/tombstoned UUID reuse fail before content-hash or secondary-index work;
  • serialize primary-map, UUID-reservation, and tenant/hash-index changes under one critical section in the in-memory adapter; require durable adapters to provide equivalent uniqueness plus tombstone or generation reservation in one transaction;
  • emit domain-separated administrative actor, tenant, and conversion-job HMAC fingerprints without raw claims, subjects, tenant identifiers, job UUIDs, filenames, document data, or signatures;
  • keep artifact-deletion failure logs free of raw job UUIDs, storage paths, and exception-controlled text;
  • remove direct secret-bearing environment binding from buyer-demo and use the shared config-tree secret boundary.

Test-first retry-race remediation

CodeRabbit identified a valid tenant-crossing retry race on predecessor head fe2e53328a0b44c1c449d724314c6c4944eb572b: Tenant A could complete an authorized dead-letter transition, a distinct Tenant B object could replace the same UUID before enqueue, and the UUID-only worker lookup could then process Tenant B’s replacement.

The current implementation closes that path by making job identifiers non-reusable. A deterministic latch-based regression pauses the accepted retry before enqueue, forces the cross-tenant replacement attempt, requires collision rejection, and proves the worker resolves only the original Tenant A object with no Tenant B lifecycle evidence. Additional tests cover exact-object save and find-or-store idempotence, collision rejection before candidate content-hash access, live and tombstoned UUID collision rejection, secondary-index ownership, delete/save interleavings, missing and cross-tenant concealment, and fail-closed adapter defaults.

Privacy-safe failure evidence

Artifact deletion remains best effort, but failure diagnostics no longer interpolate the raw conversion-job UUID or attach an exception whose message can contain a storage path or document identifier. A deterministic Log4j regression injects exception-controlled sensitive text and verifies that the emitted event contains only the fixed controlled message and no throwable.

This log hardening does not convert best-effort deletion into durable cleanup evidence.

Artifact-cleanup boundary

The current deletion path removes the tenant-owned repository record before a best-effort artifact-store deletion. An artifact-store failure is logged and does not restore the job, so orphaned bytes can remain. This PR does not implement a cleanup queue, deletion receipt, transactional outbox, retry worker, retry cadence, or aggregate cleanup evidence. Issue #263 owns the restart-safe cleanup subsystem and deterministic recovery evidence.

The remaining CodeRabbit thread on DefaultDocumentConversionService.deleteArtifact is therefore valid and intentionally unresolved. Authoritative agent, security, and deployment documentation state this limitation rather than claiming a nonexistent cleanup subsystem.

Exact current tree

Exact current head is 7f341e00ec6c0908b7b250167392fdbc743cac50.

It is a clean descendant of parent #270 exact head 26563218ae42eaa876c784fcf56b27f8cb810080, 54 commits ahead and 0 behind, with a 34-file parent-relative diff. The five commits after stale body head e25e62224aa61583f0f8df39465aae7eaf59c9f3 modify only the in-memory identifier contract, the artifact-deletion diagnostic, and their deterministic regression tests. The exact current tip is fix(security): redact artifact deletion failure logs.

No cleanup-queue implementation, global scheduling enablement, temporary/write-scoped workflow, dependency change, release change, or unrelated automation is present.

Exact-head evidence

For exact current head 7f341e00ec6c0908b7b250167392fdbc743cac50:

  • fuzz run 31071432102: completed successfully;
  • CI, Security Scan, and SAST Semgrep are absent for this stacked non-main base and are not passing;
  • the latest formal CodeRabbit review is anchored to predecessor head d55490707c24a71ae26ef208e0c0f6618606275c and is stale for this head;
  • addressed CodeRabbit findings remain resolved only where current implementation or regression evidence supports resolution;
  • one valid incomplete-artifact-cleanup thread remains unresolved and is tracked by issue [Product Gap] Complete tenant-safe document lifecycle and artifact deletion #263;
  • exact-current-head CodeRabbit, OpenCode/Noema, and Strix acceptance reviews are absent or pending;
  • counted independent write-authorized approval is absent;
  • no local mvn verify, coverage, Javadoc, or script result is represented as shareable exact-head acceptance evidence.

Queued, pending, cancelled, skipped-required, absent-required, stale-head, predecessor-head, local-only, synthetic-only, or advisory-only evidence is not passing.

Merge order and gate

Keep this PR draft. Parent #270 must satisfy every exact-head protection, receive a counted independent approval, and integrate first. Then reconcile this bounded tree onto protected main and rerun exact-head mvn -B --no-transfer-progress verify, complete Surefire/Failsafe evidence, zero missed production lines and branches, warning-free public Javadocs, CI, Security Scan, SAST, every required fuzz job, CodeRabbit, Strix/OpenCode/Noema review, zero unresolved actionable threads, a counted independent write-authorized approval, and every repository protection.

Do not manufacture checks by temporary retargeting, restore same-UUID replacement, add a global lookup or legacy mutation fallback, reintroduce a process-local cleanup queue, weaken tests, publish a release, resolve the valid cleanup thread prematurely, or bypass branch protection.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

테넌트 관리자 보안

Layer / File(s) Summary
서명 인증 및 감사 계약
AGENTS.md, docs/deployment/..., docs/security/..., src/main/java/com/clearfolio/viewer/auth/*, src/main/java/com/clearfolio/viewer/audit/*, src/main/java/com/clearfolio/viewer/security/*, src/test/java/com/clearfolio/viewer/audit/*, src/test/java/com/clearfolio/viewer/auth/*, src/test/java/com/clearfolio/viewer/config/*
관리자 요청은 강한 HMAC 기반 signed tenant claims와 ADMIN_READ 또는 ADMIN_WRITE 권한을 사용합니다. 감사 로그는 actor·tenant·job 식별자를 도메인별 HMAC fingerprint로 기록합니다. tenant claims secret은 config-tree mount에서 로드합니다.
테넌트 범위 저장소 및 원자적 상태 변경
src/main/java/com/clearfolio/viewer/repository/*, src/main/java/com/clearfolio/viewer/service/*, src/test/java/com/clearfolio/viewer/repository/*, src/test/java/com/clearfolio/viewer/service/*
저장소와 서비스가 tenant-scoped 조회·삭제·dead-letter 재시도를 제공합니다. 삭제 후에만 artifact를 정리하고, 승인된 재시도만 worker에 enqueue합니다. primary map과 content-hash index 갱신을 직렬화하고 stale index와 UUID 충돌을 fail-closed 처리합니다.
관리자 엔드포인트 연결 및 검증
src/main/java/com/clearfolio/viewer/controller/AdminController.java, src/test/java/com/clearfolio/viewer/controller/*
목록·삭제·재시도 API가 signed headers와 tenant context를 사용합니다. 누락 대상은 404, 부적격 재시도는 409, 승인된 재시도는 202로 매핑합니다. 인증 실패와 작업 결과를 감사 로그에 기록합니다.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant AdminClient
  participant AdminController
  participant TenantAccessService
  participant DefaultDocumentConversionService
  participant InMemoryConversionJobRepository
  participant ConversionJobStateStore
  participant AdministrativeAuditLogger

  AdminClient->>AdminController: signed tenant claims and admin permission
  AdminController->>TenantAccessService: requireSigned
  TenantAccessService-->>AdminController: TenantContext
  AdminController->>DefaultDocumentConversionService: tenant-scoped operation
  DefaultDocumentConversionService->>InMemoryConversionJobRepository: tenant and job identifier
  InMemoryConversionJobRepository-->>DefaultDocumentConversionService: scoped result
  DefaultDocumentConversionService->>ConversionJobStateStore: tenant-bound retry transition
  ConversionJobStateStore-->>DefaultDocumentConversionService: retry outcome
  AdminController->>AdministrativeAuditLogger: action, outcome, HTTP status
  AdminController-->>AdminClient: scoped HTTP response
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning 대부분의 요구사항을 구현했지만, 재시도와 동일 UUID 교체의 동시성 경합을 해결하지 않아 핵심 보안 요구사항을 충족하지 못합니다 [#266]. 서로 다른 작업의 동일 UUID 교체를 거부하거나 불변 tenant/generation 디스패치 계약을 도입하고 결정적 회귀 테스트를 추가하십시오.
Docstring Coverage ⚠️ Warning Docstring coverage is 28.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed 구현, 설정, 문서, OpenAPI 계약 및 테스트 변경은 관리자 인가와 테넌트 범위 보안 목표에 직접 관련됩니다.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 관리자 엔드포인트에 서명된 테넌트 클레임을 요구하는 핵심 변경을 명확하고 간결하게 설명합니다.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/admin-endpoint-auth-clean

Comment @coderabbitai help to get the list of available commands.

@seonghobae
seonghobae changed the base branch from fix/pii-logging-clean to fix/pii-logging-16240128950440010639 August 5, 2026 13:09

Copy link
Copy Markdown
Collaborator Author

@opencode-agent Rebuild this stacked PR cleanly on authoritative parent #270 exact head 06452c3f39f2deb38d31d189e46de4b25512baa0. Current implementation head 808e57d6006f1ac332e98ec32d7c156ded4f48dd is a divergent descendant of the retired parent history and is not merge evidence.

Preserve only the bounded administrative-authorization slice represented by the current 31-file diff: signed-claim verification, least-privilege admin:read/admin:write, tenant-scoped list/delete/retry contracts, concealed missing/cross-tenant outcomes including null UUIDs, atomic repository/state-store mutations, secondary-index consistency, privacy-safe domain-separated administrative audit evidence, config-tree secret loading, OpenAPI/deployment contract, realistic concurrency/security tests, and authoritative documentation. Reconcile shared CHANGELOG.md, AuditPseudonymizer.java, and AuditPseudonymizerTest.java against parent #270 rather than overwriting its Netty evidence. Do not add temporary workflows, repair scripts, unrelated dependency changes, or predecessor-parent files.

Use a clean parent-based branch history or replace the current branch only after the complete tree is ready. Run the strongest available local mvn -B --no-transfer-progress verify and relevant contract tests, keep the PR draft, and report the exact new head. After the clean rebuild, require fresh exact-head CI/security/fuzz/review evidence when the stack can target protected main; do not count current divergent-head or predecessor results.

@seonghobae
seonghobae changed the base branch from fix/pii-logging-16240128950440010639 to fix/pii-logging-clean August 5, 2026 13:11
@seonghobae
seonghobae changed the base branch from fix/pii-logging-clean to fix/pii-logging-16240128950440010639 August 5, 2026 13:15

Copy link
Copy Markdown
Collaborator Author

@opencode-agent @cwl-noema-review Review exact stacked head 808e57d6006f1ac332e98ec32d7c156ded4f48dd against parent #270 exact 06452c3f39f2deb38d31d189e46de4b25512baa0 without treating absent Actions as passing.

Manual review found that both ConversionJobRepository.findByTenantAndId and findByTenantAndContentHash retained adapter defaults that called global lookup and filtered only after materializing a job. That contradicted the branch's fail-closed modular-adapter contract. Test-first commits now prove neither global lookup path is invoked; both defaults return empty until a durable adapter implements scoped predicates, and InMemoryConversionJobRepository explicitly implements owned identifier and tenant+hash lookup. Verify the tests, Javadocs, shared-lock usage, null/blank/missing/cross-tenant concealment, and that no legitimate standalone or administrative caller silently relies on the old global fallback. Update the authoritative administrative-authorization documentation and one existing CHANGELOG.md Security entry if wording is incomplete, without duplicating sections.

Keep Draft. Parent #270 must integrate first; then this branch must be reconciled onto protected main and rerun exact-head Maven verify, zero missed line/branch coverage, warning-free Javadocs, CI, Security, SAST, fuzz, CodeRabbit, Strix/OpenCode/Noema, unresolved-thread, independent-approval, and branch-protection gates.

Copy link
Copy Markdown
Collaborator Author

@opencode-agent Parent update: rebuild the tenant-scoped administrative authorization slice directly on #270 exact head 91091ddc212dac328ff36696f56c0a15c21407f4, not retired parent 06452c3f39f2deb38d31d189e46de4b25512baa0. Preserve the bounded 31-file security slice only: signed claims, least-privilege admin:read/admin:write, tenant-scoped list/delete/retry, concealed missing/cross-tenant/null-UUID outcomes, atomic state transitions, tenant-bound secondary indexes, domain-separated privacy-safe audit evidence, config-tree secrets, deployment/OpenAPI contracts, and security/concurrency tests. Reconcile shared AGENTS.md, CHANGELOG.md, AuditPseudonymizer, DefaultDocumentConversionService, InMemoryConversionJobRepository, and their tests against the parent rather than overwriting #270's audit-key, Netty/SBOM, filesystem TOCTOU, zero-coverage, or warning-free-Javadoc contracts. Use a clean parent-based history, keep the PR draft, run mvn -B --no-transfer-progress verify, and report the exact rebuilt head. Do not add temporary/write-scoped workflows or count current divergent-head/predecessor evidence.

Copy link
Copy Markdown
Collaborator Author

@opencode-agent @cwl-noema-review Rebuild this draft as a clean descendant of authoritative parent #270 exact head 26563218ae42eaa876c784fcf56b27f8cb810080.

Fresh GitHub comparison shows current head 808e57d6006f1ac332e98ec32d7c156ded4f48dd is diverged: 53 commits ahead, 84 behind, merge base 5261356ac34e6545bce947ba0bcf2b1ce9f9be67. Preserve only the bounded 31-file administrative-authorization slice: signed tenant claims; least-privilege admin:read/admin:write; tenant-scoped list/delete/retry; atomic owned mutations; tenant-bound indexes; null/missing/cross-tenant concealment; privacy-safe domain-separated audit evidence; config-tree secret loading; deployment/OpenAPI contracts; deterministic security and concurrency tests; and authoritative documentation.

Reconcile shared CHANGELOG.md, AuditPseudonymizer.java, and AuditPseudonymizerTest.java without overwriting the parent's privacy, Netty 4.1.136.Final, deterministic SBOM/attribution, exact-head, zero-missed-line/branch, warning-free Javadoc, and fail-closed Maven report-evidence contracts. Do not add temporary workflows, repair scripts, unrelated dependencies, release changes, or predecessor-parent files.

Implement test-first, run the strongest available mvn -B --no-transfer-progress verify and relevant contract tests, keep the PR draft, and report the exact rebuilt head. Do not count the current CodeRabbit status or absent Actions as acceptance evidence.

Copy link
Copy Markdown
Collaborator Author

@opencode-agent Reconstruct this draft as a clean descendant of authoritative parent exact head 26563218ae42eaa876c784fcf56b27f8cb810080 now, without waiting for #270 to merge and without introducing a replacement PR or temporary workflow.

Required bounded operation:

  1. Preserve only the intended administrative tenant-authorization slice described in this PR: signed-claim fail-closed construction, admin:read/admin:write, tenant-scoped list and atomic delete/retry contracts, tenant-bound secondary indexes, privacy-safe administrative audit evidence, and their tests/docs.
  2. Rebase/rebuild the branch so the parent exact SHA is its merge base and the branch is 0 commits behind that parent. Do not carry predecessor fix(security): pseudonymize policy override audit identifiers #267/fix(security): require signed tenant claims on admin endpoints #266 history, stale SBOM copies, earlier Netty commits, one-shot repair workflows, dependency drift, or unrelated release/automation changes.
  3. Reconcile—not overwrite—the parent's AuditPseudonymizer, AuditPseudonymizerTest, CHANGELOG.md, privacy documentation, Netty 4.1.136.Final, deterministic SBOM/attribution, strict Maven report-evidence parser, exact-head/synthetic-merge CI, zero-missed line/branch coverage, and warning-free Javadoc contracts.
  4. Preserve the recently verified missing-UUID fail-closed behavior for scoped lookup/delete/retry and all concurrency/index ownership regressions.
  5. Run mvn -B --no-transfer-progress verify, python -m pytest -q scripts, and the authoritative dependency/evidence checks on the reconstructed head. No skipped tests, coverage relaxation, fallback global lookup, legacy unscoped mutation, or raw identifier/token logging.
  6. Keep the PR draft. Report the exact new head, parent comparison, bounded changed-file list, test totals, coverage/Javadoc evidence, and absence of temporary/write-scoped workflows. Request fresh CodeRabbit/OpenCode/Noema review only after the branch is clean and same-head checks are available.

Do not merge, retarget to main, publish, release, or bypass the independent approval gate.

@seonghobae
seonghobae force-pushed the fix/admin-endpoint-auth-clean branch from 808e57d to 3796fac Compare August 6, 2026 02:56

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@opencode-agent @cwl-noema-review Please independently review exact current head fe2e53328a0b44c1c449d724314c6c4944eb572b only, treating the divergent predecessor head and all predecessor results as stale.

This draft is now a clean descendant of parent #270 exact 26563218ae42eaa876c784fcf56b27f8cb810080, 2 commits ahead and 0 behind, mergeable, with the same bounded 31-file administrative-authorization slice. Verify signed tenant claims, strong-verifier fail-closed behavior, admin:read/admin:write least privilege, tenant-scoped list/delete/retry boundaries, concealed missing/null/cross-tenant outcomes, fail-closed adapter defaults without global lookup or legacy mutation, atomic repository and state-store behavior, secondary-index collision/replacement/concurrency invariants, domain-separated audit pseudonyms, config-tree secret sourcing, OpenAPI/deployment contracts, complete public Javadocs, and preservation of every #270 privacy/Netty/SBOM/report-evidence contract.

The exact head has successful fuzz run 31067103470 and successful CodeRabbit commit status; unresolved inline threads are zero. Repository-local CI, Security Scan, and SAST did not run for the stacked base, so they are explicitly absent-required rather than passing. Do not substitute parent or predecessor results. A full exact-head acceptance run remains mandatory after #270 integrates and the bounded slice targets protected main.

Submit actionable path-specific findings or a formal exact-head review after your own inspection. Parent integration and a counted independent write-authorized approval remain separate mandatory gates.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

@seonghobae I will review PR #268 at exact head fe2e53328a0b44c1c449d724314c6c4944eb572b.

I will treat predecessor-head results as stale. I will treat absent CI, Security Scan, and SAST results as not passing.

⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@opencode-agent @cwl-noema-review Independently review exact current head fe2e53328a0b44c1c449d724314c6c4944eb572b relative to parent #270 exact 26563218ae42eaa876c784fcf56b27f8cb810080. The clean stack is 2 commits ahead, 0 behind, mergeable, and contains exactly the bounded 31-file administrative-authorization slice.

Verify fail-closed signed-claim construction and freshness/signature/permission checks; admin:read/admin:write; tenant-scoped listing before object materialization; repository/state/service compatibility defaults that never call global lookup or legacy mutations; atomic tenant-scoped delete/retry ordering; null-ID/missing/cross-tenant concealment; tenant-bound content-hash index ownership and one-critical-section concurrency; UUID collision/replacement/stale-index/monitor races; exact enqueue and artifact-cleanup conditions; domain-separated actor and tenant audit pseudonyms without raw claims, subject, tenant, filename, content, or signatures; config-tree-only secret sourcing; and beginner-readable public Javadocs.

The branch currently has successful exact-head fuzz run 31067103470; CI, Security Scan, and SAST are absent for this stacked base and must not be inferred from parent or predecessor evidence. Keep Draft behind #270, report exact-head findings or advisory evidence, and do not infer counted independent approval.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

head='fe2e53328a0b44c1c449d724314c6c4944eb572b'
parent='26563218ae42eaa876c784fcf56b27f8cb810080'

printf '%s\n' '== Current commit and parent relationship =='
git show -s --format='HEAD %H%nParents %P%nSubject %s' "$head"
printf 'merge-base '
git merge-base "$head" "$parent"
printf 'ahead/behind '
git rev-list --left-right --count "$parent...$head"

printf '%s\n' '== Parent-relative changed paths =='
git diff --name-status "$parent" "$head"

printf '%s\n' '== Changed-file structural outline =='
git diff --name-only "$parent" "$head" | while IFS= read -r path; do
  case "$path" in
    *.java)
      printf '\n--- %s ---\n' "$path"
      ast-grep outline "$path" --items all --view compact 2>/dev/null || true
      ;;
  esac
done

printf '%s\n' '== Security boundary symbols in changed files =='
git diff --name-only "$parent" "$head" | while IFS= read -r path; do
  case "$path" in
    *.java|*.yml|*.yaml|*.properties|*.md|*.json)
      rg -n -i -C 2 \
        'findAllByTenantId|findByTenantAnd(Id|ContentHash)|deleteByTenantAndId|retryDeadLetteredForTenant|TenantContext|admin:(read|write)|AuditPseudonymizer|HMAC|secret|X-Clearfolio|content.?hash' \
        "$path" || true
      ;;
  esac
done

Length of output: 89305


🏁 Script executed:

#!/bin/bash
set -euo pipefail

print_range() {
  local path="$1" start="$2" end="$3"
  printf '\n===== %s, lines %s-%s =====\n' "$path" "$start" "$end"
  sed -n "${start},${end}p" "$path" | nl -ba -v "$start"
}

print_range src/main/java/com/clearfolio/viewer/auth/TenantAccessService.java 65 190
print_range src/main/java/com/clearfolio/viewer/controller/AdminController.java 45 285
print_range src/main/java/com/clearfolio/viewer/audit/AdministrativeAuditLogger.java 1 230
print_range src/main/java/com/clearfolio/viewer/repository/ConversionJobRepository.java 1 175
print_range src/main/java/com/clearfolio/viewer/repository/ConversionJobStateStore.java 1 135
print_range src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java 1 385
print_range src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java 210 335
print_range docs/deployment/clearfolio-buyer-connector.openapi.yaml 75 140
print_range docs/deployment/clearfolio-buyer-connector.openapi.yaml 250 390

printf '\n===== Administrative mappings and legacy permission/header references =====\n'
rg -n -C 3 \
  '@(GetMapping|DeleteMapping|PostMapping)|admin:(read|write)|job:retry|X-Clearfolio-Operator-Id|retryDeadLetteredConversionJob' \
  src/main/java docs/deployment/clearfolio-buyer-connector.openapi.yaml

printf '\n===== Audit logger call sites and logging statements =====\n'
rg -n -C 3 \
  'administrativeAuditLogger|auditLogger|logger\.(info|warn|error|debug|trace)|recordDenied' \
  src/main/java/com/clearfolio/viewer/controller/AdminController.java \
  src/main/java/com/clearfolio/viewer/audit/AdministrativeAuditLogger.java

Length of output: 2090


🏁 Script executed:

#!/bin/bash
set -euo pipefail

print_range() {
  local path="$1" start="$2" end="$3"
  printf '\n===== %s, lines %s-%s =====\n' "$path" "$start" "$end"
  sed -n "${start},${end}p" "$path"
}

print_range src/main/java/com/clearfolio/viewer/auth/TenantAccessService.java 65 190
print_range src/main/java/com/clearfolio/viewer/controller/AdminController.java 45 285
print_range src/main/java/com/clearfolio/viewer/audit/AdministrativeAuditLogger.java 1 230
print_range src/main/java/com/clearfolio/viewer/repository/ConversionJobRepository.java 1 175
print_range src/main/java/com/clearfolio/viewer/repository/ConversionJobStateStore.java 1 135
print_range src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java 1 385
print_range src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java 210 335
print_range docs/deployment/clearfolio-buyer-connector.openapi.yaml 75 140
print_range docs/deployment/clearfolio-buyer-connector.openapi.yaml 250 390

printf '\n===== Administrative mappings and legacy permission/header references =====\n'
rg -n -C 3 \
  '@(GetMapping|DeleteMapping|PostMapping)|admin:(read|write)|job:retry|X-Clearfolio-Operator-Id|retryDeadLetteredConversionJob' \
  src/main/java docs/deployment/clearfolio-buyer-connector.openapi.yaml

printf '\n===== Audit logger call sites and logging statements =====\n'
rg -n -C 3 \
  'administrativeAuditLogger|auditLogger|logger\.(info|warn|error|debug|trace)|recordDenied' \
  src/main/java/com/clearfolio/viewer/controller/AdminController.java \
  src/main/java/com/clearfolio/viewer/audit/AdministrativeAuditLogger.java

Length of output: 82816


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '===== Worker enqueue and job lookup implementations ====='
rg -n -C 5 \
  '\benqueue\s*\(|\bclaimForProcessing\s*\(|\bfindById\s*\(' \
  src/main/java

printf '%s\n' '===== Retry, replacement, and stale-observation test coverage ====='
rg -n -i -C 6 \
  'retry.*(replacement|concurrent|race|stale)|replacement.*retry|same.?UUID|sharedJobId|computeIfPresent|enqueue' \
  src/test/java/com/clearfolio/viewer

printf '%s\n' '===== Full tenant retry and concurrency test files ====='
for path in \
  src/test/java/com/clearfolio/viewer/service/TenantScopedRetryContractTest.java \
  src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryConcurrencyTest.java \
  src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryTenantScopeTest.java
do
  printf '\n--- %s ---\n' "$path"
  cat "$path"
done

Length of output: 170541


@seonghobae I found one actionable exact-head issue.

[P1] Serialize tenant retry with same-UUID replacement
retryDeadLetteredForTenant transitions the job through jobs.computeIfPresent, but it does not use jobIndexLock. save can replace the same UUID under jobIndexLock. After the retry reports ACCEPTED, DefaultDocumentConversionService enqueues only the UUID. DefaultConversionWorker later resolves that UUID through an unscoped findById.

A concurrent sequence can therefore occur:

  1. Tenant A retries its dead-lettered job.
  2. The repository changes Tenant A's job to SUBMITTED.
  3. A save replaces the UUID with Tenant B's job.
  4. The retry path enqueues the shared UUID.
  5. The worker resolves and processes Tenant B's replacement.

This breaks the tenant-bound retry invariant. It can also process a replacement that the retry caller did not authorize. Use the same critical section for the tenant-scoped retry transition and same-UUID replacement. Add a deterministic test that forces replacement after the retry transition but before enqueue or worker lookup.

Affected code: src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java, Line 305; src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java, Line 261; src/main/java/com/clearfolio/viewer/service/DefaultConversionWorker.java, Line 196.

I verified the stated parent relationship and inspected the signed-claim, least-privilege, fail-closed adapter-default, list, delete, audit, secret-source, and index boundaries. CI, Security Scan, and SAST remain absent-required. I did not treat them as passing.

You are interacting with an AI system.

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@opencode-agent @cwl-noema-review Independently inspect exact current head eea9c4c4442742d314bf532736f067e905a1e1f3 relative to parent #270 exact 26563218ae42eaa876c784fcf56b27f8cb810080.

Two review-discovered defects were fixed test-first and must be verified, not assumed:

  1. The clean reconstruction had reintroduced stale AGENTS.md guidance that weakened the authoritative mvn -B --no-transfer-progress verify, zero-skips, exact-head, Netty, JaCoCo, and warning-free public-Javadoc contracts and restored obsolete sibling names. The current diff must retain every parent gate and current CWL name, changing only the valid tenant-claims config-tree guidance.
  2. Administrative audit logs emitted raw conversion-job UUIDs. Test commit 0e57c13401df51967c644980b4bb8d0d5db82cb9 requires jobFingerprint and forbids both the raw UUID and jobId=. Verify the production job-specific HMAC domain, actor/tenant/job domain separation, absent/unavailable markers, beginner-readable Javadocs, captured-log exclusions, and the updated security decision.

Run mvn -B --no-transfer-progress verify and python -m pytest -q scripts on this exact head. Report exact test totals, zero failures/errors/skips, zero missed production lines/branches, warning-free Javadocs, changed-file scope, and any actionable path-specific finding. The exact-head fuzz run is still pending; CI, Security Scan, and SAST are absent on the stacked base and must not be inferred from parent or predecessor evidence. Keep Draft, do not merge, and do not infer counted independent approval.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

head='eea9c4c4442742d314bf532736f067e905a1e1f3'
parent='26563218ae42eaa876c784fcf56b27f8cb810080'

echo '== Exact commit relationship =='
git show -s --format='HEAD %H%nParents %P%nSubject %s' "$head"
printf 'merge-base: '; git merge-base "$head" "$parent"
printf 'ahead/behind: '; git rev-list --left-right --count "$parent...$head"

echo '== Parent-relative changed paths =='
git diff --name-status "$parent" "$head"

echo '== AGENTS.md parent-relative diff =='
git diff --unified=80 "$parent" "$head" -- AGENTS.md

echo '== Audit-related changed paths =='
git diff --name-only "$parent" "$head" | rg -i '(audit|admin|security|decision|pseudonym|test)' || true

echo '== Audit source/test structural outlines =='
fd -t f -i 'AuditPseudonymizer|AdministrativeAuditLogger' src | while IFS= read -r f; do
  echo "--- $f ---"
  ast-grep outline "$f" --items all --view compact || true
done

echo '== Job fingerprint, domains, raw jobId logging, and captured-log assertions =='
rg -n -i -C 4 \
  'jobFingerprint|conversion[- ]job|jobId=|jobId\b|HMAC|domain|unavailable|absent|captur|appender' \
  src/main/java src/test/java docs AGENTS.md 2>/dev/null || true

echo '== Security decision and current CWL/config-tree terms =='
rg -n -i -C 3 \
  'administrative|audit|pseudonym|config[- ]tree|CWL|Netty|JaCoCo|javadoc|exact.head|zero.skip|verify' \
  AGENTS.md docs src/main/resources src/test/java 2>/dev/null || true

echo '== Recent exact-head test and production diffs =='
git show --stat --oneline --decorate "$head"
git diff --unified=40 "${head}^..${head}" -- \
  ':(glob)**/*AuditPseudonymizer*.java' \
  ':(glob)**/*AdministrativeAuditLogger*.java' \
  ':(glob)**/*Audit*.java' \
  ':(glob)**/*security*' \
  ':(glob)**/*Security*' \
  ':(glob)**/AGENTS.md' || true

Length of output: 202329


🏁 Script executed:

#!/bin/bash
set -euo pipefail

head='eea9c4c4442742d314bf532736f067e905a1e1f3'
parent='26563218ae42eaa876c784fcf56b27f8cb810080'

echo '== Relationship and changed-file count =='
printf 'merge-base='; git merge-base "$head" "$parent"
printf 'ahead_behind='; git rev-list --left-right --count "$parent...$head"
git diff --name-only "$parent" "$head" | tee /tmp/pr268-changed-files.txt
printf 'changed_file_count='; wc -l < /tmp/pr268-changed-files.txt

echo '== AGENTS.md diff and parent-equivalence outside config-tree terms =='
git diff --unified=12 "$parent" "$head" -- AGENTS.md
echo '-- parent and head AGENTS.md SHA-256 --'
git show "$parent:AGENTS.md" | sha256sum
git show "$head:AGENTS.md" | sha256sum
echo '-- changed AGENTS.md line numbers at head --'
git diff -U0 "$parent" "$head" -- AGENTS.md | rg '^@@|^[+-][^+-]' || true

echo '== Locate administrative audit implementation and decision =='
fd -t f -a . src/main/java src/test/java docs/security | rg \
  '(AdministrativeAuditLogger|AuditPseudonymizer|administrative-authorization)' | sort

echo '== AdministrativeAuditLogger source and test =='
for f in \
  src/main/java/com/clearfolio/viewer/audit/AdministrativeAuditLogger.java \
  src/test/java/com/clearfolio/viewer/audit/AdministrativeAuditLoggerTest.java \
  docs/security/2026-08-05-administrative-authorization.md
do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    nl -ba "$f"
  fi
done

echo '== All AdministrativeAuditLogger call sites with context =='
rg -n -C 6 '\b(record|recordHeaders|jobFingerprint|actorFingerprint|tenantFingerprint)\s*\(' \
  src/main/java/com/clearfolio/viewer \
  -g '*.java'

echo '== Administrative-audit raw identifier fields or logs =='
rg -n -i -C 4 \
  '(jobId=|jobId\b|conversion.?job|logger\.(info|warn|error|debug|trace)|log\.(info|warn|error|debug|trace))' \
  src/main/java/com/clearfolio/viewer/controller/AdminController.java \
  src/main/java/com/clearfolio/viewer/audit \
  src/test/java/com/clearfolio/viewer/audit 2>/dev/null || true

echo '== Parent-relative audit/doc/test diffs =='
git diff --unified=20 "$parent" "$head" -- \
  src/main/java/com/clearfolio/viewer/audit \
  src/test/java/com/clearfolio/viewer/audit \
  docs/security/2026-08-05-administrative-authorization.md

Length of output: 2709


🏁 Script executed:

#!/bin/bash
set -euo pipefail

head='eea9c4c4442742d314bf532736f067e905a1e1f3'
parent='26563218ae42eaa876c784fcf56b27f8cb810080'

echo '== Changed-file count and paths =='
changed_files="$(git diff --name-only "$parent" "$head")"
printf '%s\n' "$changed_files"
printf 'changed_file_count='
printf '%s\n' "$changed_files" | awk 'NF { count += 1 } END { print count }'

echo '== AGENTS.md parent-relative diff =='
git diff --unified=16 "$parent" "$head" -- AGENTS.md

echo '== Relevant source and documentation files =='
fd -t f -a . src/main/java src/test/java docs/security | rg \
  '(AdministrativeAuditLogger|AuditPseudonymizer|administrative-authorization)' | sort

for f in \
  src/main/java/com/clearfolio/viewer/audit/AdministrativeAuditLogger.java \
  src/test/java/com/clearfolio/viewer/audit/AdministrativeAuditLoggerTest.java \
  docs/security/2026-08-05-administrative-authorization.md
do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    nl -ba "$f"
  fi
done

echo '== Administrative logger call sites =='
rg -n -C 6 \
  'administrativeAuditLogger\.(record|recordHeaders)|\b(record|recordHeaders)\s*\(' \
  src/main/java/com/clearfolio/viewer \
  -g '*.java'

echo '== Administrative audit raw-ID and logging references =='
rg -n -i -C 4 \
  '(jobId=|jobId\b|conversion.?job|logger\.(info|warn|error|debug|trace)|log\.(info|warn|error|debug|trace))' \
  src/main/java/com/clearfolio/viewer/controller/AdminController.java \
  src/main/java/com/clearfolio/viewer/audit \
  src/test/java/com/clearfolio/viewer/audit 2>/dev/null || true

echo '== Parent-relative audit, test, and decision diff =='
git diff --unified=24 "$parent" "$head" -- \
  src/main/java/com/clearfolio/viewer/audit \
  src/test/java/com/clearfolio/viewer/audit \
  docs/security/2026-08-05-administrative-authorization.md

Length of output: 8601


🏁 Script executed:

#!/bin/bash
set -euo pipefail

show_file() {
  file="$1"
  if [ -f "$file" ]; then
    echo "--- $file ---"
    sed -n '1,340p' "$file"
  else
    echo "--- missing: $file ---"
  fi
}

show_file src/main/java/com/clearfolio/viewer/audit/AdministrativeAuditLogger.java
show_file src/test/java/com/clearfolio/viewer/audit/AdministrativeAuditLoggerTest.java
show_file src/main/java/com/clearfolio/viewer/security/AuditPseudonymizer.java
show_file src/test/java/com/clearfolio/viewer/security/AuditPseudonymizerTest.java
show_file docs/security/2026-08-05-administrative-authorization.md

echo '== AdministrativeAuditLogger construction and invocation sites =='
rg -n -C 8 \
  '(new AdministrativeAuditLogger|AdministrativeAuditLogger|administrativeAuditLogger\.(record|recordHeaders))' \
  src/main/java/com/clearfolio/viewer \
  -g '*.java'

echo '== Every direct logger call in the administrative controller and audit package =='
rg -n -C 4 \
  '(logger|log)\.(info|warn|error|debug|trace)\s*\(' \
  src/main/java/com/clearfolio/viewer/controller/AdminController.java \
  src/main/java/com/clearfolio/viewer/audit \
  -g '*.java' || true

echo '== Exact job field names in audit implementation and captured-log tests =='
rg -n -C 5 \
  '(jobFingerprint|jobId=|jobId\b|raw-job|raw.*UUID|UUID)' \
  src/main/java/com/clearfolio/viewer/audit \
  src/test/java/com/clearfolio/viewer/audit \
  docs/security/2026-08-05-administrative-authorization.md \
  -g '*.java' -g '*.md'

echo '== Parent-relative final audit/privacy commits =='
git log --format='%H %s' --ancestry-path \
  26563218ae42eaa876c784fcf56b27f8cb810080..eea9c4c4442742d314bf532736f067e905a1e1f3
git show --format=fuller --stat 0e57c13401df51967c644980b4bb8d0d5db82cb9 \
  2e6622ebe93c7f03bd019fe308aa346cecf8a908 \
  4d453dfdbf76cd3cde2eaafbee875aee92eaa773 \
  eea9c4c4442742d314bf532736f067e905a1e1f3

Length of output: 80286


@seonghobae I inspected exact head eea9c4c4442742d314bf532736f067e905a1e1f3 against parent 26563218ae42eaa876c784fcf56b27f8cb810080.

The merge base is the requested parent. The branch is 7 commits ahead and 0 commits behind. The parent-relative diff contains 31 files.

I verified the two requested corrections:

  • AGENTS.md changes only the tenant-claims secret guidance. It now requires the config-tree mount selected by CLEARFOLIO_SECRET_CONFIG_DIR. The diff does not weaken the parent acceptance-gate guidance.
  • AdministrativeAuditLogger uses jobFingerprint, not jobId=, in its log template. It applies AuditPseudonymizer.forAdministrativeJob(...).
  • AuditPseudonymizer defines separate HMAC domains for administrative actor, tenant, and conversion-job values.
  • AdministrativeAuditLoggerTest captures formatted log output. It rejects both the raw UUID and jobId=.
  • The security decision now explicitly excludes raw job UUIDs and defines the three separate fingerprint domains.

I found one remaining actionable issue from the prior exact-head review.

[P1] Serialize tenant retry with same-UUID replacement
InMemoryConversionJobRepository.retryDeadLetteredForTenant still does not use the shared jobIndexLock. A concurrent save can replace the same UUID after the retry transition and before worker resolution. The retry path then enqueues a UUID that DefaultConversionWorker resolves through unscoped findById. The worker can process the replacement job for another tenant.

The commits after fe2e53328a0b44c1c449d724314c6c4944eb572b do not modify InMemoryConversionJobRepository.java, DefaultDocumentConversionService.java, or DefaultConversionWorker.java. Therefore, this finding remains on exact head eea9c4c4442742d314bf532736f067e905a1e1f3.

Use one shared critical section for same-UUID save and tenant-scoped retry. Add a deterministic test that replaces the UUID after the owned retry transition and before worker lookup.

I could not run mvn -B --no-transfer-progress verify or python -m pytest -q scripts. The sandbox does not permit compiler or interpreter execution. Therefore, test totals, zero failures/errors/skips, zero missed lines/branches, and warning-free Javadocs are unverified. The pending fuzz run, absent CI, Security Scan, and SAST are not passing evidence. I did not approve or merge this draft.

⚠️ Action not completed

Head commit changed.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Collaborator Author

@opencode-agent Fix the newly confirmed exact-head P1 retry/replacement race on fe2e53328a0b44c1c449d724314c6c4944eb572b test-first, without broadening this 31-file administrative-authorization slice.

CodeRabbit independently established this concrete sequence:

  1. Tenant A's dead-lettered job transitions to SUBMITTED through retryDeadLetteredForTenant.
  2. Before the service enqueue or worker lookup completes, save replaces the same UUID with Tenant B's job.
  3. The service enqueues only the UUID.
  4. DefaultConversionWorker later resolves that UUID through unscoped findById and can process Tenant B's replacement.

Required bounded correction:

  1. Add a deterministic RED concurrency test that pauses the accepted retry after the authorized transition and forces a same-UUID cross-tenant replacement before enqueue/claim. It must prove the replacement is never enqueued or processed and no unauthorized lifecycle event is emitted.
  2. Make conversion-job identifiers immutable while a live/tombstoned record owns them, or introduce an equivalent immutable tenant+generation dispatch receipt. A lock around only jobs.computeIfPresent is insufficient because replacement can occur after that lock is released and before UUID-only worker resolution.
  3. Prefer the smallest coherent design consistent with issue [Product Gap] Complete tenant-safe document lifecycle and artifact deletion #263's non-reusable identifier/lifecycle-generation contract. save may remain idempotent for the exact same stored object if required, but it must fail closed on a distinct live object with the same UUID; deletion/recreation semantics must be explicit and tested rather than silently replacing ownership.
  4. Rework existing tests that currently normalize direct live same-UUID replacement as valid behavior. Preserve the useful delete-vs-create critical-section test, tenant/hash index consistency, collision detection, missing/null/cross-tenant concealment, and adapter fail-closed defaults.
  5. Ensure tenant-scoped retry, enqueue, and worker claim remain bound to the authorized immutable object/generation. Do not add a global lookup fallback, raw tenant/job logging, temporary workflow, dependency, or unrelated refactor.
  6. Run mvn -B --no-transfer-progress verify, python -m pytest -q scripts, and all repository concurrency/security contracts. Preserve zero failures/errors/skips, zero missed production lines/branches, and warning-free public Javadocs.
  7. Keep Draft. Report the exact new head, parent comparison, changed files, deterministic interleaving evidence, and any migration/compatibility consequence. Request fresh CodeRabbit/OpenCode/Noema review only after the new exact head exists.

Do not dismiss the finding merely because #263 will later add a durable outbox; this PR must not ship a tenant-crossing retry path.

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@opencode-agent @cwl-noema-review Review exact current head 73d1f77c67e417c51aa7548b7a43d4aea6ad4463 relative to parent #270 exact 26563218ae42eaa876c784fcf56b27f8cb810080. This is the bounded 32-file administrative authorization and immutable job-identity slice; process-local cleanup queue/scheduler files have been removed and issue #263 remains the durable deletion-lifecycle owner.

Verify signed-claim fail-closed construction, admin:read/admin:write, tenant-scoped list/delete/retry boundaries, missing/null/cross-tenant concealment, privacy-safe actor/tenant/job HMAC domains, config-tree secret sourcing, fail-closed adapter defaults, tenant-bound content indexes, permanent UUID reservation, exact-object-only save idempotence, live/tombstoned collision rejection before index work, deterministic delete/save and retry/save interleavings, worker enqueue resolving only the authorized original object, and durable adapter tombstone/generation requirements. Verify no cleanup queue, global scheduling enablement, temporary workflow, global lookup fallback, raw tenant/job/token logging, dependency drift, release, or unrelated automation remains.

Run or inspect focused tests plus mvn -B --no-transfer-progress verify and python -m pytest -q scripts; preserve zero failures/errors/skips, zero missed production lines/branches, and warning-free public Javadocs. Report exact-head actionable findings or no-blocker advisory evidence only. Keep Draft and do not infer counted independent approval.

Copy link
Copy Markdown
Collaborator Author

@opencode-agent Correct the authoritative evidence at exact current head 8713d204e6f38b27d8fd00baf7c1c1e6acd13873 without broadening this administrative-authorization slice.

The process-local ArtifactCleanupQueue implementation and scheduler were removed, but three authoritative documents still claim they exist. Production DefaultDocumentConversionService.deleteArtifact still logs and swallows artifact-store deletion failures; therefore the branch cannot claim deduplicated pending work, retry cadence, aggregate cleanup metrics, cleanup-aware store decoration, or associated tests.

Required bounded correction:

  1. In AGENTS.md, remove the Durable cleanup boundary section that mandates a configured ArtifactCleanupQueue. Replace it only with an honest gap statement: issue [Product Gap] Complete tenant-safe document lifecycle and artifact deletion #263 owns restart-safe deletion receipts/outbox/cleanup worker; this PR does not implement that subsystem.
  2. In docs/deployment/2026-07-02-buyer-deployment-integration-playbook.md, remove every claim that failed deletion is retained in a process-local queue, that the artifact store is wrapped by ArtifactCleanupQueue, that a retry cadence exists, or that deletion failures produce aggregate evidence. State instead that owned repository deletion precedes best-effort artifact removal, failure can leave orphaned bytes, and production cutover remains blocked on [Product Gap] Complete tenant-safe document lifecycle and artifact deletion #263 durable cleanup/outbox evidence.
  3. In docs/security/2026-08-05-administrative-authorization.md, remove the same nonexistent queue, metrics, evidence, and verification claims. Preserve the valid tenant-authorization ordering and explicitly record failed artifact deletion durability as an unresolved [Product Gap] Complete tenant-safe document lifecycle and artifact deletion #263 boundary.
  4. Keep the valid immutable/non-reusable conversion-job UUID contract and the corrected CHANGELOG.md lifecycle bullets. Do not add a scheduler, process-local queue, dependency, workflow, release change, or new production subsystem.
  5. Add or update a deterministic documentation-contract test only if one already governs these authoritative claims; otherwise keep this documentation-only correction minimal.
  6. Run the authoritative mvn -B --no-transfer-progress verify and preserve zero failures/errors/skips, zero missed production lines/branches, and warning-free public Javadocs. Report the exact resulting head and do not claim absent CI/Security/SAST evidence.

@seonghobae
seonghobae force-pushed the fix/admin-endpoint-auth-clean branch from 8713d20 to 73d1f77 Compare August 6, 2026 04:07
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

head='73d1f77c67e417c51aa7548b7a43d4aea6ad4463'
parent='26563218ae42eaa876c784fcf56b27f8cb810080'

echo '== Exact relationship =='
git show -s --format='HEAD %H%nParents %P%nSubject %s' "$head"
printf 'merge_base='; git merge-base "$head" "$parent"
printf 'ahead_behind='; git rev-list --left-right --count "$parent...$head"

echo '== Parent-relative paths =='
git diff --name-status "$parent" "$head"
printf 'changed_file_count='
git diff --name-only "$parent" "$head" | awk 'NF { n++ } END { print n+0 }'

echo '== Prohibited cleanup/scheduling/workflow paths and annotations =='
git diff --name-only "$parent" "$head" | rg -i \
  '(ArtifactCleanupQueue|ClearfolioViewerApplication|\.github/workflows|workflow|scheduler|scheduling)' || true
rg -n -C2 '`@EnableScheduling`|ArtifactCleanupQueue|retryPending' \
  src/main/java src/test/java 2>/dev/null || true

echo '== Primary implementation outlines =='
for f in \
  src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobStateStore.java \
  src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java \
  src/main/java/com/clearfolio/viewer/service/DefaultConversionWorker.java \
  src/main/java/com/clearfolio/viewer/auth/TenantAccessService.java \
  src/main/java/com/clearfolio/viewer/controller/AdminController.java \
  src/main/java/com/clearfolio/viewer/audit/AdministrativeAuditLogger.java \
  src/main/java/com/clearfolio/viewer/security/AuditPseudonymizer.java
do
  echo "--- $f ---"
  ast-grep outline "$f" --items all --view compact 2>/dev/null || true
done

echo '== UUID reservation, retry, dispatch, tenant/default paths =='
rg -n -C5 \
  'reserved|reservation|tombstone|generation|existing == job|jobs\.put|putIfAbsent|findOrStoreByContentHash|retryDeadLetteredForTenant|deleteByTenantAndId|jobIndexLock|enqueue\s*\(|claimForProcessing|findById\s*\(|findAllByTenantId|findByTenantAnd(Id|ContentHash)' \
  src/main/java/com/clearfolio/viewer \
  -g '*.java'

echo '== Deterministic ownership/concurrency tests =='
rg -n -i -C5 \
  'retry.*save|save.*retry|same.?UUID|tombston|reservation|replacement|collision|interleav|latch|enqueue|lifecycle' \
  src/test/java/com/clearfolio/viewer \
  -g '*.java'

echo '== Audit, claims, permissions, config-tree, and raw logging terms =='
rg -n -i -C3 \
  'requireSigned|admin:(read|write)|forAdministrative(Job|Actor|Tenant)|jobFingerprint|jobId=|CLEARFOLIO_SECRET_CONFIG_DIR|config.tree|X-Clearfolio|logger\.(info|warn|error|debug|trace)' \
  src/main/java src/main/resources src/test/java docs AGENTS.md \
  -g '*.java' -g '*.yml' -g '*.yaml' -g '*.md'

Length of output: 203318


🏁 Script executed:

#!/bin/bash
set -euo pipefail

head='73d1f77c67e417c51aa7548b7a43d4aea6ad4463'
parent='26563218ae42eaa876c784fcf56b27f8cb810080'

show() {
  local file="$1" start="$2" end="$3"
  echo "===== ${file} lines ${start}-${end} ====="
  nl -ba "$file" | sed -n "${start},${end}p"
}

show src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java 1 390
show src/main/java/com/clearfolio/viewer/repository/ConversionJobRepository.java 1 230
show src/main/java/com/clearfolio/viewer/repository/ConversionJobStateStore.java 1 180
show src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java 220 315
show src/main/java/com/clearfolio/viewer/service/DefaultConversionWorker.java 160 230
show src/main/java/com/clearfolio/viewer/controller/AdminController.java 45 290
show docs/security/2026-08-05-administrative-authorization.md 1 180
show docs/deployment/2026-07-02-buyer-deployment-integration-playbook.md 1 330
show AGENTS.md 75 155
show CHANGELOG.md 1 150

echo '== All cleanup-queue, cleanup retry, scheduler, and durable-cleanup assertions =='
rg -n -i -C3 \
  'ArtifactCleanupQueue|process-local.*(queue|retry)|cleanup.*(queue|retry|cadence|metric|evidence)|pending.*cleanup|`@EnableScheduling`|durable cleanup' \
  AGENTS.md CHANGELOG.md docs src/main/java src/test/java \
  -g '*.md' -g '*.java' -g '*.yml' -g '*.yaml' || true

echo '== Parent-relative diffs for source, contracts, tests, and documentation =='
git diff --unified=18 "$parent" "$head" -- \
  src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobStateStore.java \
  src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java \
  src/main/java/com/clearfolio/viewer/service/DefaultConversionWorker.java \
  src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryConcurrencyTest.java \
  src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryTenantScopeTest.java \
  src/test/java/com/clearfolio/viewer/service/TenantScopedRetryContractTest.java \
  AGENTS.md CHANGELOG.md \
  docs/deployment/2026-07-02-buyer-deployment-integration-playbook.md \
  docs/security/2026-08-05-administrative-authorization.md

echo '== Administrative mappings, permissions, and service calls =='
rg -n -C4 \
  '@(GetMapping|DeleteMapping|PostMapping)|TenantPermissions\.ADMIN_(READ|WRITE)|getJobsForTenant|deleteJob\(|retryDeadLettered\(' \
  src/main/java/com/clearfolio/viewer/controller/AdminController.java \
  src/main/java/com/clearfolio/viewer/service/DocumentConversionService.java \
  src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java

Length of output: 3264


🏁 Script executed:

#!/bin/bash
set -euo pipefail

head='73d1f77c67e417c51aa7548b7a43d4aea6ad4463'
parent='26563218ae42eaa876c784fcf56b27f8cb810080'

show() {
  local file="$1" start="$2" end="$3"
  echo "===== ${file} lines ${start}-${end} ====="
  awk -v start="$start" -v end="$end" \
    'NR >= start && NR <= end { printf "%6d  %s\n", NR, $0 }' "$file"
}

show src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java 1 390
show src/main/java/com/clearfolio/viewer/repository/ConversionJobRepository.java 1 230
show src/main/java/com/clearfolio/viewer/repository/ConversionJobStateStore.java 1 180
show src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java 220 315
show src/main/java/com/clearfolio/viewer/service/DefaultConversionWorker.java 160 230
show src/main/java/com/clearfolio/viewer/controller/AdminController.java 45 290
show docs/security/2026-08-05-administrative-authorization.md 1 180
show docs/deployment/2026-07-02-buyer-deployment-integration-playbook.md 1 330
show AGENTS.md 75 155
show CHANGELOG.md 1 150

echo '== All cleanup-queue, cleanup retry, scheduler, and durable-cleanup assertions =='
rg -n -i -C3 \
  'ArtifactCleanupQueue|process-local.*(queue|retry)|cleanup.*(queue|retry|cadence|metric|evidence)|pending.*cleanup|`@EnableScheduling`|durable cleanup' \
  AGENTS.md CHANGELOG.md docs src/main/java src/test/java \
  -g '*.md' -g '*.java' -g '*.yml' -g '*.yaml' || true

echo '== Parent-relative diffs for source, contracts, tests, and documentation =='
git diff --unified=18 "$parent" "$head" -- \
  src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobStateStore.java \
  src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java \
  src/main/java/com/clearfolio/viewer/service/DefaultConversionWorker.java \
  src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryConcurrencyTest.java \
  src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryTenantScopeTest.java \
  src/test/java/com/clearfolio/viewer/service/TenantScopedRetryContractTest.java \
  AGENTS.md CHANGELOG.md \
  docs/deployment/2026-07-02-buyer-deployment-integration-playbook.md \
  docs/security/2026-08-05-administrative-authorization.md

echo '== Administrative mappings, permissions, and service calls =='
rg -n -C4 \
  '@(GetMapping|DeleteMapping|PostMapping)|TenantPermissions\.ADMIN_(READ|WRITE)|getJobsForTenant|deleteJob\(|retryDeadLettered\(' \
  src/main/java/com/clearfolio/viewer/controller/AdminController.java \
  src/main/java/com/clearfolio/viewer/service/DocumentConversionService.java \
  src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java

Length of output: 234298


@seonghobae I reviewed exact head 73d1f77c67e417c51aa7548b7a43d4aea6ad4463 against parent 26563218ae42eaa876c784fcf56b27f8cb810080.

The requested parent is the merge base. The parent-relative scope contains 32 files.

I found one actionable issue.

[P1] Remove claims for the removed cleanup subsystem
docs/deployment/2026-07-02-buyer-deployment-integration-playbook.md Line 27, Line 76, and Line 170 state that ArtifactCleanupQueue retains failed deletions and performs bounded asynchronous retry.
docs/security/2026-08-05-administrative-authorization.md Line 34, Line 60, and Line 73 through Line 75 make the same implementation and test claims.

The reviewed source has no ArtifactCleanupQueue, cleanup retry worker, or scheduling enablement. DefaultDocumentConversionService.deleteArtifact only logs and suppresses an artifact-store deletion exception. These documents therefore overstate the current cleanup behavior and the available privacy-safe aggregate evidence.

State the current behavior accurately:

  • Tenant-owned repository deletion occurs before best-effort artifact removal.
  • A failed artifact deletion can leave orphaned bytes.
  • Issue #263 owns durable deletion receipts, an outbox, and a cleanup worker.
  • Production cutover remains blocked on durable cleanup evidence.

I verified these static boundaries:

  • AdminController requires signed claims before service access.
  • Listing uses admin:read. Delete and retry use admin:write.
  • Tenant-aware repository, state-store, and service defaults fail closed. They do not call global lookup or legacy mutation methods.
  • Tenant-scoped delete and retry conceal missing and cross-tenant objects.
  • Administrative audit output uses separate actor, tenant, and job HMAC domains. The captured-log tests reject raw UUID values and jobId=.
  • The config-tree import uses CLEARFOLIO_SECRET_CONFIG_DIR.
  • InMemoryConversionJobRepository permanently reserves UUIDs. It accepts an idempotent save only for the exact live object. It rejects distinct live and tombstoned UUID reuse before secondary-index work.
  • The deterministic retry-versus-save test rejects Tenant B’s replacement. It verifies that the enqueue path resolves Tenant A’s original object.
  • I found no process-local cleanup queue, global scheduling enablement, temporary workflow, or dependency change in the reviewed scope.

I could not run mvn -B --no-transfer-progress verify or python -m pytest -q scripts. This sandbox cannot execute repository compilers or interpreters. Test totals, zero failures/errors/skips, zero missed production lines/branches, and warning-free Javadocs are unverified. I did not treat absent CI, Security Scan, SAST, or fuzz evidence as passing. I did not approve or merge the draft.

You are interacting with an AI system.

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@opencode-agent @cwl-noema-review Independently review exact current head da03f567107ec0f6244bcc46720fb7af8bc2b2da relative to parent #270 exact 26563218ae42eaa876c784fcf56b27f8cb810080. Treat every predecessor-head comment, status, and test result as stale unless it remains directly applicable.

Verify the 32-file administrative authorization slice only: signed-claim fail-closed construction; admin:read/admin:write; tenant-scoped list/delete/retry boundaries; missing/null/cross-tenant concealment; domain-separated actor/tenant/job HMAC evidence; config-tree-only secret sourcing; fail-closed adapter defaults; tenant-bound content indexes; permanent UUID reservation; exact-live-object-only idempotent save; distinct live and tombstoned UUID collision rejection before secondary-index work; deterministic retry/save and delete/save interleavings; worker enqueue resolving only the authorized original object; and durable adapter tombstone/generation requirements.

Confirm that the removed process-local cleanup queue, global scheduling enablement, cleanup configuration, temporary workflows, global lookup fallback, raw tenant/job/token logging, dependency drift, release, and unrelated automation are absent. The documentation must truthfully identify artifact deletion as current best effort and leave restart-safe receipts/outbox/cleanup recovery to issue #263.

Same-head fuzz run 31070107997 succeeded; later duplicate fuzz activity is not additional acceptance evidence. CI, Security Scan, and SAST are absent on this stacked non-main base and must not be inferred from parent or predecessor heads. Report actionable exact-head findings or a no-blocker advisory conclusion. Keep Draft and do not infer counted independent approval.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

head='da03f567107ec0f6244bcc46720fb7af8bc2b2da'
parent='26563218ae42eaa876c784fcf56b27f8cb810080'

printf '%s\n' '== Exact relationship =='
git show -s --format='HEAD %H%nParents %P%nSubject %s' "$head"
printf 'merge_base='; git merge-base "$head" "$parent"
printf 'ahead_behind='; git rev-list --left-right --count "$parent...$head"

printf '%s\n' '== Parent-relative changed paths =='
git diff --name-status "$parent" "$head"
printf 'changed_file_count='
git diff --name-only "$parent" "$head" | awk 'NF { count++ } END { print count+0 }'

printf '%s\n' '== Prohibited scope paths =='
git diff --name-only "$parent" "$head" | rg -i \
  '(ArtifactCleanupQueue|ClearfolioViewerApplication|\.github/workflows|workflow|scheduler|scheduling|pom\.xml|build\.gradle|package\.json)' || true

printf '%s\n' '== Structural outline of changed Java files =='
git diff --name-only "$parent" "$head" | while IFS= read -r path; do
  case "$path" in
    *.java)
      printf '\n--- %s ---\n' "$path"
      ast-grep outline "$path" --items all --view compact 2>/dev/null || true
      ;;
  esac
done

Length of output: 6274


🏁 Script executed:

#!/bin/bash
set -euo pipefail

show() {
  local file="$1"
  printf '\n===== %s =====\n' "$file"
  nl -ba "$file"
}

for file in \
  src/main/java/com/clearfolio/viewer/auth/TenantAccessService.java \
  src/main/java/com/clearfolio/viewer/auth/TenantPermissions.java \
  src/main/java/com/clearfolio/viewer/controller/AdminController.java \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobStateStore.java \
  src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/service/DocumentConversionService.java \
  src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java \
  src/main/java/com/clearfolio/viewer/service/DefaultConversionWorker.java \
  src/main/java/com/clearfolio/viewer/audit/AdministrativeAuditLogger.java \
  src/main/java/com/clearfolio/viewer/security/AuditPseudonymizer.java \
  src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryConcurrencyTest.java \
  src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryTenantScopeTest.java \
  src/test/java/com/clearfolio/viewer/service/TenantScopedRetryContractTest.java \
  src/test/java/com/clearfolio/viewer/service/TenantScopedAtomicMutationBoundaryTest.java \
  src/test/java/com/clearfolio/viewer/audit/AdministrativeAuditLoggerTest.java \
  docs/security/2026-08-05-administrative-authorization.md \
  docs/deployment/2026-07-02-buyer-deployment-integration-playbook.md \
  src/main/resources/application.yml \
  src/main/resources/application-buyer-demo.yml
do
  show "$file"
done

printf '\n===== Targeted prohibited implementation and unsafe logging scan =====\n'
rg -n -i -C 3 \
  'ArtifactCleanupQueue|EnableScheduling|retryPending|findAll\s*\(|findById\s*\(|jobId=|logger\.(info|warn|error|debug|trace)|X-Clearfolio|CLEARFOLIO_TENANT_CLAIMS_HMAC_SECRET|CLEARFOLIO_SECRET_CONFIG_DIR' \
  src/main/java src/main/resources src/test/java docs AGENTS.md CHANGELOG.md \
  -g '*.java' -g '*.yml' -g '*.yaml' -g '*.md' || true

Length of output: 2552


🏁 Script executed:

#!/bin/bash
set -euo pipefail

show() {
  local file="$1"
  printf '\n===== %s =====\n' "$file"
  awk '{ printf "%6d  %s\n", NR, $0 }' "$file"
}

for file in \
  src/main/java/com/clearfolio/viewer/auth/TenantAccessService.java \
  src/main/java/com/clearfolio/viewer/auth/TenantPermissions.java \
  src/main/java/com/clearfolio/viewer/controller/AdminController.java \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobStateStore.java \
  src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/service/DocumentConversionService.java \
  src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java \
  src/main/java/com/clearfolio/viewer/service/DefaultConversionWorker.java \
  src/main/java/com/clearfolio/viewer/audit/AdministrativeAuditLogger.java \
  src/main/java/com/clearfolio/viewer/security/AuditPseudonymizer.java \
  src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryConcurrencyTest.java \
  src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryTenantScopeTest.java \
  src/test/java/com/clearfolio/viewer/service/TenantScopedRetryContractTest.java \
  src/test/java/com/clearfolio/viewer/service/TenantScopedAtomicMutationBoundaryTest.java \
  src/test/java/com/clearfolio/viewer/audit/AdministrativeAuditLoggerTest.java \
  docs/security/2026-08-05-administrative-authorization.md \
  docs/deployment/2026-07-02-buyer-deployment-integration-playbook.md \
  src/main/resources/application.yml \
  src/main/resources/application-buyer-demo.yml
do
  show "$file"
done

printf '\n===== Targeted prohibited implementation and unsafe logging scan =====\n'
rg -n -i -C 3 \
  'ArtifactCleanupQueue|EnableScheduling|retryPending|findAll\s*\(|findById\s*\(|jobId=|logger\.(info|warn|error|debug|trace)|X-Clearfolio|CLEARFOLIO_TENANT_CLAIMS_HMAC_SECRET|CLEARFOLIO_SECRET_CONFIG_DIR' \
  src/main/java src/main/resources src/test/java docs AGENTS.md CHANGELOG.md \
  -g '*.java' -g '*.yml' -g '*.yaml' -g '*.md' || true

Length of output: 196463


🏁 Script executed:

#!/bin/bash
set -euo pipefail

range() {
  local file="$1" start="$2" end="$3"
  printf '\n===== %s, lines %s-%s =====\n' "$file" "$start" "$end"
  awk -v start="$start" -v end="$end" \
    'NR >= start && NR <= end { printf "%6d  %s\n", NR, $0 }' "$file"
}

range src/main/java/com/clearfolio/viewer/auth/TenantAccessService.java 1 220
range src/main/java/com/clearfolio/viewer/controller/AdminController.java 1 340
range src/main/java/com/clearfolio/viewer/repository/ConversionJobRepository.java 1 240
range src/main/java/com/clearfolio/viewer/repository/ConversionJobStateStore.java 1 190
range src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java 1 390
range src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java 210 355
range src/main/java/com/clearfolio/viewer/service/DefaultConversionWorker.java 175 230
range src/main/java/com/clearfolio/viewer/audit/AdministrativeAuditLogger.java 1 230
range src/main/java/com/clearfolio/viewer/security/AuditPseudonymizer.java 1 260
range src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryConcurrencyTest.java 1 330
range src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryTenantScopeTest.java 55 310
range src/test/java/com/clearfolio/viewer/controller/AdminControllerTest.java 1 430
range src/test/java/com/clearfolio/viewer/auth/TenantAccessServiceStrictClaimsTest.java 1 180
range src/test/java/com/clearfolio/viewer/config/BuyerDemoSecretConfigurationTest.java 1 120

printf '\n===== Parent-relative implementation diff =====\n'
git diff --unified=16 \
  26563218ae42eaa876c784fcf56b27f8cb810080 \
  da03f567107ec0f6244bcc46720fb7af8bc2b2da -- \
  src/main/java/com/clearfolio/viewer/auth/TenantAccessService.java \
  src/main/java/com/clearfolio/viewer/controller/AdminController.java \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobStateStore.java \
  src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java \
  src/main/java/com/clearfolio/viewer/service/DocumentConversionService.java \
  src/main/java/com/clearfolio/viewer/audit/AdministrativeAuditLogger.java \
  src/main/java/com/clearfolio/viewer/security/AuditPseudonymizer.java

Length of output: 197828


@seonghobae I reviewed exact head da03f567107ec0f6244bcc46720fb7af8bc2b2da against parent 26563218ae42eaa876c784fcf56b27f8cb810080.

The merge base is the requested parent. The branch is 48 commits ahead and 0 commits behind. The parent-relative diff contains 32 files.

I found two actionable issues.

  1. [P1] Remove the raw job UUID from the artifact-deletion failure log.
    src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java, Line 334 logs jobId directly:

    log.warn("Failed to delete artifact for job {}", jobId, ex);

    The new administrative delete endpoint calls deleteArtifact(jobId) after an owned deletion. An artifact-store failure therefore writes the raw conversion-job UUID to logs. This violates the required no-raw-job-identifier boundary and conflicts with the administrative audit policy.

    Log a privacy-safe job fingerprint, or log no job identifier. Do not include the exception object if its message can contain a raw identifier.

  2. [P2] Reject a same-tenant, same-hash, distinct-object UUID collision in findOrStoreByContentHash.
    src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java, Line 89 checks the tenant-content index before it checks whether the candidate UUID is reserved.

    If a distinct candidate has the same tenant, content hash, and live UUID as the indexed job, Line 93 returns the existing job as a dedupe hit. The code does not call storeNewCandidate, so it does not reject the distinct object or check the UUID reservation first.

    This does not rebind the stored job. However, it does not meet the stated contract that a distinct live or tombstoned UUID must fail closed through findOrStoreByContentHash before secondary-index work. Check the candidate UUID reservation before the index lookup, while preserving the exact-live-object idempotence rule where required. Add a focused regression test for this same-tenant and same-content-hash collision.

I also verified these static boundaries:

  • AdminController calls requireSigned before it calls the service.
  • List requires admin:read. Delete and retry require admin:write.
  • Tenant-aware repository, state-store, and service defaults fail closed. They do not use global lookup or legacy mutation paths.
  • Delete and retry conceal missing and cross-tenant outcomes.
  • InMemoryConversionJobRepository reserves accepted UUIDs permanently. save allows only the exact stored live object.
  • The deterministic retry/save test rejects a cross-tenant same-UUID replacement after the accepted retry transition. It verifies enqueue resolution of the original Tenant A object.
  • Administrative audit logging uses separate actor, tenant, and job HMAC domains. The captured-log test excludes raw UUID values and jobId=.
  • The config uses CLEARFOLIO_SECRET_CONFIG_DIR and config-tree secret properties.
  • The deployment and security documents now state that artifact deletion is best effort. They leave durable receipts, outbox processing, and cleanup recovery to issue #263.
  • I found no changed cleanup queue, global scheduling enablement, workflow, dependency manifest, release artifact, or unrelated automation.

I could not run mvn -B --no-transfer-progress verify or python -m pytest -q scripts in this sandbox. Test totals, zero skips, JaCoCo coverage, and Javadoc warnings remain unverified.

I treated fuzz run 31070107997 as the stated successful same-head fuzz evidence. CI, Security Scan, and SAST remain absent and are not passing evidence. I did not approve or merge the draft.

You are interacting with an AI system.

Copy link
Copy Markdown
Collaborator Author

Exact-head review requested for da03f567107ec0f6244bcc46720fb7af8bc2b2da against parent 26563218ae42eaa876c784fcf56b27f8cb810080.

The authoritative documentation now states the actual cleanup boundary: this slice has no cleanup queue, deletion receipt, outbox, retry cadence, cleanup metrics, or durable recovery evidence. Artifact removal remains best effort and can leave orphaned bytes; issue #263 owns the production-grade cleanup subsystem. The remaining DefaultDocumentConversionService.deleteArtifact review thread is valid and intentionally unresolved.

Exact-head fuzz run 31070493489 succeeded. CI, Security Scan, and SAST are absent for the stacked base and are not passing. CodeRabbit, OpenCode/Noema, Strix, and independent approval remain required.

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@opencode-agent @cwl-noema-review Please independently inspect exact current head da03f567107ec0f6244bcc46720fb7af8bc2b2da. Treat predecessor-head results as stale. Preserve Draft status and report exact-head findings only.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

head='da03f567107ec0f6244bcc46720fb7af8bc2b2da'
parent='26563218ae42eaa876c784fcf56b27f8cb810080'

echo '== Exact relationship =='
git show -s --format='HEAD %H%nParents %P%nSubject %s' "$head"
printf 'merge_base='; git merge-base "$head" "$parent"
printf 'ahead_behind='; git rev-list --left-right --count "$parent...$head"

echo '== Parent-relative changed paths =='
git diff --name-status "$parent" "$head"
printf 'changed_file_count='
git diff --name-only "$parent" "$head" | awk 'NF { count++ } END { print count+0 }'

echo '== Relevant source outlines =='
for file in \
  src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java \
  src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/service/DefaultConversionWorker.java \
  src/main/java/com/clearfolio/viewer/audit/AdministrativeAuditLogger.java \
  src/main/java/com/clearfolio/viewer/controller/AdminController.java
do
  echo "--- $file ---"
  ast-grep outline "$file" --items all --view compact 2>/dev/null || true
done

echo '== Artifact deletion, UUID collision, retry dispatch, and worker lookup =='
rg -n -C 8 \
  'deleteArtifact\s*\(|Failed to delete artifact|findOrStoreByContentHash\s*\(|storeNewCandidate\s*\(|reserved|reservation|existing == job|retryDeadLetteredForTenant\s*\(|enqueue\s*\(|findById\s*\(' \
  src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java \
  src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/service/DefaultConversionWorker.java

echo '== Relevant test coverage =='
rg -n -i -C 6 \
  'same.*tenant.*same.*hash|findOrStoreByContentHash|tombston|collision|retry.*save|save.*retry|artifact.*delete|raw.*UUID|jobId=' \
  src/test/java/com/clearfolio/viewer \
  -g '*.java'

echo '== Parent-relative diffs for inspected code and tests =='
git diff --unified=24 "$parent" "$head" -- \
  src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java \
  src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/service/DefaultConversionWorker.java \
  ':(glob)src/test/java/**/*ConversionJobRepository*Test.java' \
  ':(glob)src/test/java/**/*TenantScopedRetry*Test.java'

echo '== Prohibited scope and cleanup assertions =='
git diff --name-only "$parent" "$head" | rg -i \
  '(ArtifactCleanupQueue|ClearfolioViewerApplication|\.github/workflows|workflow|scheduler|scheduling|pom\.xml|build\.gradle|package\.json)' || true
rg -n -i -C 3 \
  'ArtifactCleanupQueue|EnableScheduling|cleanup queue|retry cadence|cleanup metrics|durable cleanup' \
  AGENTS.md CHANGELOG.md docs src/main/java src/test/java \
  -g '*.md' -g '*.java' -g '*.yml' -g '*.yaml' || true

Length of output: 186236


@seonghobae I reviewed exact head da03f567107ec0f6244bcc46720fb7af8bc2b2da against parent 26563218ae42eaa876c784fcf56b27f8cb810080.

The requested parent is the merge base. The branch is 48 commits ahead and 0 commits behind. The parent-relative diff contains 32 files.

I found two actionable issues.

  1. [P1] Remove the raw conversion-job UUID from the artifact-deletion failure log.
    src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java, Line 334 logs the raw identifier:

    log.warn("Failed to delete artifact for job {}", jobId, ex);

    The tenant-scoped administrative delete path calls deleteArtifact(jobId) after the owned repository deletion. A deletion failure therefore logs the raw conversion-job UUID. The exception object can also contain a raw identifier from the artifact store.

    Log no identifier, or log a privacy-safe fingerprint. Do not attach an exception object unless its message is safe for the audit logging boundary.

  2. [P2] Validate candidate UUID ownership before the content-hash deduplication lookup.
    src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java, Lines 89 through 101 evaluate the tenant-content index before storeNewCandidate(ConversionJob) validates the candidate UUID.

    If a distinct candidate has the same tenant, content hash, and live UUID as the indexed job, matchesContentIndex(...) succeeds and the method returns the stored job as a deduplication hit. It does not reject the distinct candidate.

    This bypasses the documented contract that findOrStoreByContentHash rejects a distinct live or tombstoned UUID before secondary-index work. Validate the candidate identifier reservation before the index lookup. Preserve exact-object idempotence where it is required. Add a focused regression test for a same-tenant, same-content-hash, distinct-object UUID collision.

I also verified these exact-head static conditions:

  • The diff contains no cleanup queue, scheduler enablement, workflow, or dependency-manifest change.
  • The documentation states artifact removal is best effort and assigns durable cleanup recovery to issue #263.
  • The repository reserves accepted UUIDs permanently.
  • save permits only the exact live object as an idempotent save.
  • Tenant-aware repository and service defaults fail closed.
  • Administrative audit tests reject raw UUID output and jobId=.
  • The deterministic retry/save test rejects a cross-tenant replacement and observes the original tenant’s object.

I could not run Maven or Python commands in this sandbox. Test totals, failures, skips, JaCoCo coverage, and Javadoc warnings remain unverified.

You are interacting with an AI system.

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@opencode-agent @cwl-noema-review Please independently inspect exact current head e25e62224aa61583f0f8df39465aae7eaf59c9f3. Treat all predecessor-head evidence as stale. The exact-head fuzz run is currently queued and is not passing; CI, Security Scan, and SAST remain absent on the stacked base. Preserve Draft status and return exact-head findings only.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

head='e25e62224aa61583f0f8df39465aae7eaf59c9f3'
parent='26563218ae42eaa876c784fcf56b27f8cb810080'
prior='da03f567107ec0f6244bcc46720fb7af8bc2b2da'

echo '== Exact relationship =='
git show -s --format='HEAD %H%nParents %P%nSubject %s' "$head"
printf 'merge_base='; git merge-base "$head" "$parent"
printf 'ahead_behind='; git rev-list --left-right --count "$parent...$head"

echo '== Parent-relative changed paths =='
git diff --name-status "$parent" "$head"
printf 'changed_file_count='
git diff --name-only "$parent" "$head" | awk 'NF { n++ } END { print n+0 }'

echo '== Changes since the prior reviewed head =='
git log --oneline --ancestry-path "${prior}..${head}"
git diff --name-status "$prior" "$head"
git diff --unified=32 "$prior" "$head" -- \
  src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java \
  src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java \
  src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryTenantScopeTest.java \
  src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryConcurrencyTest.java \
  src/test/java/com/clearfolio/viewer/service/TenantScopedRetryContractTest.java \
  src/test/java/com/clearfolio/viewer/service/TenantScopedAtomicMutationBoundaryTest.java \
  docs/security/2026-08-05-administrative-authorization.md \
  CHANGELOG.md

echo '== Changed Java file structure =='
git diff --name-only "$parent" "$head" | while IFS= read -r path; do
  case "$path" in
    *.java)
      echo "--- $path ---"
      ast-grep outline "$path" --items all --view compact 2>/dev/null || true
      ;;
  esac
done

echo '== Inspect sensitive implementation paths =='
for file in \
  src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java \
  src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/service/DefaultConversionWorker.java \
  src/main/java/com/clearfolio/viewer/controller/AdminController.java \
  src/main/java/com/clearfolio/viewer/auth/TenantAccessService.java \
  src/main/java/com/clearfolio/viewer/audit/AdministrativeAuditLogger.java \
  src/main/java/com/clearfolio/viewer/security/AuditPseudonymizer.java \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobStateStore.java \
  src/main/java/com/clearfolio/viewer/service/DocumentConversionService.java
do
  echo "--- $file ---"
  nl -ba "$file"
done

echo '== Focused implementation and test search =='
rg -n -i -C 5 \
  'deleteArtifact|Failed to delete artifact|logger\.(info|warn|error|debug|trace)|jobId=|jobFingerprint|findOrStoreByContentHash|storeNewCandidate|reserved|tombstone|existing == job|same.?tenant|same.?hash|collision|retryDeadLetteredForTenant|enqueue\s*\(|claimForProcessing|findById\s*\(|requireSigned|admin:(read|write)|forAdministrative(Job|Actor|Tenant)|CLEARFOLIO_SECRET_CONFIG_DIR|ArtifactCleanupQueue|EnableScheduling' \
  src/main/java src/test/java src/main/resources docs AGENTS.md CHANGELOG.md \
  -g '*.java' -g '*.yml' -g '*.yaml' -g '*.md' || true

echo '== Scope exclusions =='
git diff --name-only "$parent" "$head" | rg -i \
  '(ArtifactCleanupQueue|ClearfolioViewerApplication|\.github/workflows|workflow|scheduler|scheduling|pom\.xml|build\.gradle|package\.json)' || true

Length of output: 14120


🏁 Script executed:

#!/bin/bash
set -euo pipefail

head='e25e62224aa61583f0f8df39465aae7eaf59c9f3'
parent='26563218ae42eaa876c784fcf56b27f8cb810080'
prior='da03f567107ec0f6244bcc46720fb7af8bc2b2da'

numbered() {
  file="$1"
  echo "===== $file ====="
  awk '{ printf "%6d  %s\n", NR, $0 }' "$file"
}

echo '== Exact source and test material for prior findings =='
for file in \
  src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java \
  src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java \
  src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryTenantScopeTest.java \
  src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryConcurrencyTest.java \
  src/test/java/com/clearfolio/viewer/service/TenantScopedRetryContractTest.java \
  src/test/java/com/clearfolio/viewer/service/TenantScopedAtomicMutationBoundaryTest.java
do
  numbered "$file"
done

echo '== Exact diffs for prior findings =='
git diff --unified=60 "$prior" "$head" -- \
  src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java \
  src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java \
  src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryTenantScopeTest.java \
  src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryConcurrencyTest.java

echo '== Authorization, audit, contract, config, and documentation boundaries =='
for file in \
  src/main/java/com/clearfolio/viewer/controller/AdminController.java \
  src/main/java/com/clearfolio/viewer/auth/TenantAccessService.java \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobStateStore.java \
  src/main/java/com/clearfolio/viewer/service/DocumentConversionService.java \
  src/main/java/com/clearfolio/viewer/audit/AdministrativeAuditLogger.java \
  src/main/java/com/clearfolio/viewer/security/AuditPseudonymizer.java \
  src/main/resources/application.yml \
  src/main/resources/application-buyer-demo.yml \
  docs/security/2026-08-05-administrative-authorization.md \
  docs/deployment/2026-07-02-buyer-deployment-integration-playbook.md
do
  numbered "$file"
done

echo '== Targeted calls, logs, validation order, and test assertions =='
rg -n -C 8 \
  'deleteArtifact\s*\(|Failed to delete artifact|log\.(info|warn|error|debug|trace)\s*\(|findOrStoreByContentHash\s*\(|storeNewCandidate\s*\(|ensure.*(Reservation|Identifier)|reserved|tombston|existing == job|same.?tenant|same.?hash|distinct.*(job|object)|retryDeadLetteredForTenant\s*\(|enqueue\s*\(|claimForProcessing\s*\(|findById\s*\(|requireSigned\s*\(|ADMIN_(READ|WRITE)|jobFingerprint|forAdministrative(Job|Actor|Tenant)|CLEARFOLIO_SECRET_CONFIG_DIR|ArtifactCleanupQueue|EnableScheduling' \
  src/main/java src/test/java src/main/resources docs AGENTS.md CHANGELOG.md \
  -g '*.java' -g '*.yml' -g '*.yaml' -g '*.md' || true

echo '== Parent-relative diffs for all security-critical implementation =='
git diff --unified=24 "$parent" "$head" -- \
  src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java \
  src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/controller/AdminController.java \
  src/main/java/com/clearfolio/viewer/auth/TenantAccessService.java \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobStateStore.java \
  src/main/java/com/clearfolio/viewer/service/DocumentConversionService.java \
  src/main/java/com/clearfolio/viewer/audit/AdministrativeAuditLogger.java \
  src/main/java/com/clearfolio/viewer/security/AuditPseudonymizer.java

echo '== Prohibited parent-relative paths =='
git diff --name-only "$parent" "$head" | rg -i \
  '(ArtifactCleanupQueue|ClearfolioViewerApplication|\.github/workflows|workflow|scheduler|scheduling|pom\.xml|build\.gradle|package\.json)' || true

Length of output: 143916


@seonghobae I reviewed exact head e25e62224aa61583f0f8df39465aae7eaf59c9f3.

The requested parent 26563218ae42eaa876c784fcf56b27f8cb810080 is the merge base. The branch is 49 commits ahead and 0 commits behind the parent. The parent-relative diff contains 32 files.

I found two actionable exact-head issues.

  1. [P1] Remove the raw conversion-job UUID and exception from the artifact-deletion failure log.
    src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java, deleteArtifact(UUID), still contains:

    log.warn("Failed to delete artifact for job {}", jobId, ex);

    The tenant-scoped administrative delete path calls this method after the owned repository deletion. This log writes the raw conversion-job UUID. The exception message can also contain the raw UUID from the artifact store.

    Log no job identifier and do not attach the exception object unless its message is safe. A privacy-safe fingerprint is also acceptable if it uses the dedicated job HMAC domain.

  2. [P2] Validate the candidate UUID before the content-hash deduplication lookup.
    src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java, findOrStoreByContentHash(ConversionJob), still checks jobsByTenantAndContentHash before storeNewCandidate(ConversionJob) validates the candidate UUID reservation.

    A distinct candidate with the same tenant, content hash, and live UUID as the indexed object takes the deduplication path. The method returns the stored object without rejecting the distinct candidate. This bypasses the documented contract that a distinct live or tombstoned UUID must fail closed through findOrStoreByContentHash.

    Check the candidate UUID reservation before the content-index lookup. Preserve idempotence only when the candidate is the exact stored object. Add a focused same-tenant, same-hash, distinct-object collision test.

I verified these static conditions:

  • AdminController requires signed claims before it calls the service.
  • List uses admin:read. Delete and retry use admin:write.
  • Tenant-aware repository, state-store, and service defaults fail closed.
  • The audit logger uses separate actor, tenant, and job HMAC domains.
  • The parent-relative scope contains no cleanup queue, global scheduling enablement, workflow, or dependency-manifest change.
  • Permanent UUID reservation prevents the earlier retry/replacement dispatch race.

I could not run Maven or Python in this sandbox. Test totals, skips, coverage, and Javadoc warnings remain unverified. The exact-head fuzz run is queued. CI, Security Scan, and SAST are absent. I did not treat any of these checks as passing.

You are interacting with an AI system.

Copy link
Copy Markdown
Collaborator Author

@opencode-agent Implement the two CodeRabbit findings on exact current branch head after RED commits 6a3e33c7dc0f2b3fda125dd992121bf60e6d8237 and 41eed2d7897d6ca15666bc58cac99188321313f8. Do not rewrite or remove those tests.

Bounded production work only:

  1. In DefaultDocumentConversionService.deleteArtifact, replace the dynamic UUID/exception warning with one controlled constant warning. Do not include the job UUID, storage path, exception message, exception object, tenant, filename, or token.
  2. In InMemoryConversionJobRepository.findOrStoreByContentHash, validate candidate identity ownership under jobIndexLock before reading the candidate content hash or consulting the tenant-content secondary index:
    • exact same live object returns an idempotent created=false result;
    • a distinct live object with the same UUID throws Conversion job identifier collision. even when tenant and content hash match the indexed job;
    • a tombstoned UUID throws before content-hash/index work;
    • a new UUID may still deduplicate to an existing canonical tenant/hash job without reserving the rejected candidate UUID.
  3. Preserve every existing permanent-reservation, delete/save, retry/save, tenant concealment, lifecycle event, and fail-closed adapter contract.
  4. Run the two focused new test classes first, then mvn -B --no-transfer-progress verify and python -m pytest -q scripts. Preserve zero failures/errors/skips, zero missed production lines/branches, and warning-free public Javadocs.
  5. Commit only the two production fixes plus any minimal test correction proven necessary by compilation. Do not add cleanup queues, schedulers, workflows, dependencies, releases, broad refactors, or new product claims.
  6. Keep Draft and report the exact final head, focused/full test totals, coverage/Javadoc evidence, and parent-relative scope. Request fresh exact-head CodeRabbit/OpenCode/Noema review after verification.

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@opencode-agent @cwl-noema-review Please independently review exact current head 7f341e00ec6c0908b7b250167392fdbc743cac50 against parent #270 exact head 26563218ae42eaa876c784fcf56b27f8cb810080.

Focus on the five commits after predecessor head e25e62224aa61583f0f8df39465aae7eaf59c9f3: exact-live-object findOrStoreByContentHash idempotence, rejection of live or reserved UUID collisions before candidate content-hash/index access, and artifact-deletion failure log redaction with deterministic non-disclosure evidence. Confirm that these changes preserve tenant-scoped authorization, immutable lifecycle identity, retry-race protection, config-tree secret loading, zero raw identifier leakage, and the parent acceptance contracts.

Exact-head fuzz run 31071432102 succeeded. CI, Security Scan, and SAST are absent on this stacked non-main base and must not be inferred as passing. The incomplete durable artifact-cleanup finding remains valid, intentionally unresolved, and owned by #263; do not resolve it or treat fixed-message logging as durable cleanup. Submit actionable findings or an exact-head advisory review only. Do not infer independent human approval, weaken protection, or bypass stack order.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

head='7f341e00ec6c0908b7b250167392fdbc743cac50'
parent='26563218ae42eaa876c784fcf56b27f8cb810080'
prior='e25e62224aa61583f0f8df39465aae7eaf59c9f3'

echo '== Exact relationship =='
git show -s --format='HEAD %H%nParents %P%nSubject %s' "$head"
printf 'merge_base='; git merge-base "$head" "$parent"
printf 'ahead_behind='; git rev-list --left-right --count "$parent...$head"

echo '== Commits since prior reviewed head =='
git log --format='%H %s' --ancestry-path "${prior}..${head}"

echo '== Parent-relative changed paths =='
git diff --name-status "$parent" "$head"
printf 'changed_file_count='
git diff --name-only "$parent" "$head" | awk 'NF { count++ } END { print count+0 }'

echo '== Changes since prior head =='
git diff --name-status "$prior" "$head"
git diff --unified=36 "$prior" "$head" -- \
  src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java \
  src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryTenantScopeTest.java \
  src/test/java/com/clearfolio/viewer/service/TenantScopedAtomicMutationBoundaryTest.java

echo '== Target file outlines =='
for f in \
  src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java \
  src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryTenantScopeTest.java \
  src/test/java/com/clearfolio/viewer/service/TenantScopedAtomicMutationBoundaryTest.java \
  src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryConcurrencyTest.java
do
  echo "--- $f ---"
  ast-grep outline "$f" --items all --view compact 2>/dev/null || true
done

echo '== Candidate ownership, index lookup, retry dispatch, and log redaction =='
rg -n -C 8 \
  'findOrStoreByContentHash\s*\(|storeNewCandidate\s*\(|reservation|reserved|tombston|existing == job|contentHash\s*\(|jobsByTenantAndContentHash|deleteArtifact\s*\(|Failed to delete artifact|log\.(info|warn|error|debug|trace)\s*\(|retryDeadLetteredForTenant\s*\(|enqueue\s*\(|claimForProcessing\s*\(|findById\s*\(' \
  src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java \
  src/main/java/com/clearfolio/viewer/service/DefaultConversionWorker.java \
  src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryTenantScopeTest.java \
  src/test/java/com/clearfolio/viewer/service/TenantScopedAtomicMutationBoundaryTest.java \
  src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryConcurrencyTest.java

echo '== Authorization, audit domains, config-tree, and scope exclusions =='
rg -n -C 4 \
  'requireSigned\s*\(|ADMIN_(READ|WRITE)|forAdministrative(Job|Actor|Tenant)|jobFingerprint|CLEARFOLIO_SECRET_CONFIG_DIR|findAllByTenantId|findByTenantAnd(Id|ContentHash)|deleteByTenantAndId|retryDeadLetteredForTenant' \
  src/main/java src/main/resources \
  -g '*.java' -g '*.yml' -g '*.yaml'

echo '== Prohibited paths and implementation markers =='
git diff --name-only "$parent" "$head" | rg -i \
  '(ArtifactCleanupQueue|ClearfolioViewerApplication|\.github/workflows|workflow|scheduler|scheduling|pom\.xml|build\.gradle|package\.json)' || true
rg -n -i -C 2 \
  'ArtifactCleanupQueue|EnableScheduling|cleanup queue|retry cadence|cleanup metrics' \
  src/main/java src/test/java docs AGENTS.md CHANGELOG.md \
  -g '*.java' -g '*.md' -g '*.yml' -g '*.yaml' || true

Length of output: 151278


🏁 Script executed:

#!/bin/bash
set -euo pipefail

head='7f341e00ec6c0908b7b250167392fdbc743cac50'
prior='e25e62224aa61583f0f8df39465aae7eaf59c9f3'

show_file() {
  local file="$1"
  echo "===== $file ====="
  nl -ba "$file"
}

show_file src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryIdentifierContractTest.java
show_file src/test/java/com/clearfolio/viewer/service/ArtifactDeletionPrivacyTest.java

echo '== New-test diffs =='
git diff --unified=80 "$prior" "$head" -- \
  src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryIdentifierContractTest.java \
  src/test/java/com/clearfolio/viewer/service/ArtifactDeletionPrivacyTest.java

echo '== Full find-or-store and identifier-reservation helpers =='
sed -n '45,125p' src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java | nl -ba -v45
sed -n '350,430p' src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java | nl -ba -v350

echo '== Full artifact-delete helper and all service log calls =='
sed -n '215,345p' src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java | nl -ba -v215
rg -n -C 3 'log\.(trace|debug|info|warn|error)\s*\(' \
  src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java

echo '== Retry/save deterministic regression and collision assertions =='
rg -n -i -C 8 \
  'retry.*save|save.*retry|replacement|sharedJobId|reserved|tombston|findOrStore.*collision|content.?hash.*access|contentHash.*throw|lifecycle' \
  src/test/java/com/clearfolio/viewer/repository \
  -g '*.java'

echo '== New tests: static imports and test dependencies =='
rg -n -C 1 \
  '^(import static|import )|`@Test`|assert(Throws|Equals|False|True|Same)|fail\(' \
  src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryIdentifierContractTest.java \
  src/test/java/com/clearfolio/viewer/service/ArtifactDeletionPrivacyTest.java

Length of output: 2407


🏁 Script executed:

#!/bin/bash
set -euo pipefail

head='7f341e00ec6c0908b7b250167392fdbc743cac50'
prior='e25e62224aa61583f0f8df39465aae7eaf59c9f3'

show_file() {
  local file="$1"
  echo "===== $file ====="
  awk '{ printf "%6d  %s\n", NR, $0 }' "$file"
}

show_range() {
  local file="$1" start="$2" end="$3"
  echo "===== $file lines $start-$end ====="
  awk -v start="$start" -v end="$end" \
    'NR >= start && NR <= end { printf "%6d  %s\n", NR, $0 }' "$file"
}

show_file src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryIdentifierContractTest.java
show_file src/test/java/com/clearfolio/viewer/service/ArtifactDeletionPrivacyTest.java

echo '== New-test diffs =='
git diff --unified=80 "$prior" "$head" -- \
  src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryIdentifierContractTest.java \
  src/test/java/com/clearfolio/viewer/service/ArtifactDeletionPrivacyTest.java

show_range src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java 45 125
show_range src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java 350 430

echo '== Artifact-delete helper and direct service log calls =='
show_range src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java 215 345
rg -n -C 3 'log\.(trace|debug|info|warn|error)\s*\(' \
  src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java

echo '== Retry/save deterministic regression and collision assertions =='
rg -n -i -C 8 \
  'retry.*save|save.*retry|replacement|sharedJobId|reserved|tombston|findOrStore.*collision|content.?hash.*access|contentHash.*throw|lifecycle' \
  src/test/java/com/clearfolio/viewer/repository \
  -g '*.java'

echo '== New-test imports and assertions =='
rg -n -C 1 \
  '^(import static|import )|`@Test`|assert(Throws|Equals|False|True|Same)|fail\(' \
  src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryIdentifierContractTest.java \
  src/test/java/com/clearfolio/viewer/service/ArtifactDeletionPrivacyTest.java

Length of output: 135107


@seonghobae Exact-head review completed for 7f341e00ec6c0908b7b250167392fdbc743cac50.

I found no new actionable finding in the requested changes.

The merge base is parent #270 at 26563218ae42eaa876c784fcf56b27f8cb810080. The branch is 54 commits ahead and 0 commits behind. The parent-relative diff contains 34 files. The increase from 32 files is the two focused regression tests added for the reviewed findings.

I verified the five commits after e25e62224aa61583f0f8df39465aae7eaf59c9f3:

  • InMemoryConversionJobRepository.findOrStoreByContentHash now validates candidate.getJobId() under jobIndexLock before it reads candidate.getContentHash() or accesses the tenant-content index.
  • The exact stored live object returns created=false.
  • A distinct live object with the same UUID fails with Conversion job identifier collision., including when tenant and content hash match.
  • A reserved tombstoned UUID also fails before content-hash and index work.
  • A new UUID can still deduplicate to the canonical tenant-plus-content-hash job without reserving the rejected candidate UUID.
  • InMemoryConversionJobRepositoryIdentifierContractTest uses a candidate that throws on getContentHash(). This verifies collision rejection occurs before candidate hash access.
  • DefaultDocumentConversionService.deleteArtifact logs only the controlled message Artifact deletion failed after an authorized job deletion..
  • ArtifactDeletionPrivacyTest captures the log event. It rejects the raw UUID and exception-controlled storage message. It also verifies that no throwable is attached.

I also verified the existing static boundaries:

  • Administrative endpoints require signed claims before service access.
  • Listing requires admin:read.
  • Delete and retry require admin:write.
  • Tenant-aware repository, state-store, and service defaults fail closed.
  • Permanent UUID reservation prevents retry dispatch from resolving a replacement job.
  • Actor, tenant, and job audit pseudonyms use separate HMAC domains.
  • Config-tree secret loading uses CLEARFOLIO_SECRET_CONFIG_DIR.
  • The diff contains no cleanup queue, scheduler enablement, workflow, dependency-manifest, release, or unrelated automation change.
  • Documentation still identifies artifact deletion as best effort. Durable cleanup remains an unresolved #263 responsibility.

I accept the stated successful exact-head fuzz run 31071432102 as available fuzz evidence. CI, Security Scan, and SAST remain absent. I did not treat them as passing.

I could not run Maven or Python commands in this sandbox. Exact test totals, zero skips, JaCoCo coverage, and Javadoc warnings remain unverified. I did not infer independent human approval.

You are interacting with an AI system.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant